/*
 * Copyright 2010-2012 VMware and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springsource.loaded;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

// TODO try to recall why I created ConstantPoolChecker2, what was up with ConstantPoolChecker?

// https://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html
/**
 * Enables us to check things quickly in the constant pool. This version accumulates the class references and the method
 * references, for classes that start with 'j' (we want to catch: java/lang). It skips everything it can and the end
 * result is a list of class references and a list of method references. The former look like this 'a/b/C' whilst the
 * latter look like this 'java/lang/Foo.bar' (the descriptor for the method is not included). Interface methods are
 * skipped.
 *
 * @author Andy Clement
 * @since 0.7.3
 */
public class ConstantPoolChecker2 {

	private static final boolean DEBUG = false;

	private final static byte CONSTANT_Utf8 = 1;

	private final static byte CONSTANT_Integer = 3;

	private final static byte CONSTANT_Float = 4;

	private final static byte CONSTANT_Long = 5;

	private final static byte CONSTANT_Double = 6;

	private final static byte CONSTANT_Class = 7;

	private final static byte CONSTANT_String = 8;

	private final static byte CONSTANT_Fieldref = 9;

	private final static byte CONSTANT_Methodref = 10;

	private final static byte CONSTANT_InterfaceMethodref = 11;

	private final static byte CONSTANT_NameAndType = 12;

	private final static byte CONSTANT_MethodHandle = 15;

	private final static byte CONSTANT_MethodType = 16;

	private final static byte CONSTANT_InvokeDynamic = 18;

	// Test entry point just goes through all the code in the bin folder
	public static void main(String[] args) throws Exception {
		File[] fs = new File("./bin").listFiles();
		//		File[] fs = new File("../testdata-groovy/bin").listFiles();
		//		File[] fs = new File("/Users/aclement/grailsreload/foo/target/classes").listFiles();

		checkThemAll(fs);
		System.out.println("total=" + total / 1000000d);
	}

	private static void checkThemAll(File[] fs) throws Exception {
		for (File f : fs) {
			if (f.isDirectory()) {
				checkThemAll(f.listFiles());
			}
			else if (f.getName().endsWith(".class")) {
				System.out.println(f);
				byte[] data = Utils.loadFromStream(new FileInputStream(f));
				long stime = System.nanoTime();
				References refs = getReferences(data);
				total += (System.nanoTime() - stime);
				System.out.println(refs.referencedClasses);
				System.out.println(refs.referencedMethods);
			}
		}
	}

	static long total = 0;

	//	ClassFile {
	//    	u4 magic;
	//    	u2 minor_version;
	//    	u2 major_version;
	//    	u2 constant_pool_count;
	//    	cp_info constant_pool[constant_pool_count-1];
	//    	u2 access_flags;
	//    	u2 this_class;
	//    	u2 super_class;
	//    	u2 interfaces_count;
	//    	u2 interfaces[interfaces_count];
	//    	u2 fields_count;
	//    	field_info fields[fields_count];
	//    	u2 methods_count;
	//    	method_info methods[methods_count];
	//    	u2 attributes_count;
	//    	attribute_info attributes[attributes_count];
	//    }

	static References getReferences(byte[] bytes) {
		ConstantPoolChecker2 cpc2 = new ConstantPoolChecker2(bytes);
		return new References(cpc2.slashedclassname, cpc2.referencedClasses, cpc2.referencedMethods);
	}

	static class References {

		String slashedClassName;

		List<String> referencedClasses;

		List<String> referencedMethods;

		References(String slashedClassName, List<String> rc, List<String> rm) {
			this.slashedClassName = slashedClassName;
			this.referencedClasses = rc;
			this.referencedMethods = rm;
		}
	}

	// Filled with strings and int[]
	private Object[] cpdata;

	private int cpsize;

	private int[] type;

	// Does not need to be a set as there are no dups in the ConstantPool (for a class from a decent compiler...)
	private List<String> referencedClasses = new ArrayList<String>();

	private List<String> referencedMethods = new ArrayList<String>();

	private String slashedclassname;

	private ConstantPoolChecker2(byte[] bytes) {
		readConstantPool(bytes);
		computeReferences();
	}

	public void computeReferences() {
		for (int i = 0; i < cpsize; i++) {
			switch (type[i]) {
				case CONSTANT_Class:
					int classindex = ((Integer) cpdata[i]);
					String classname = (String) cpdata[classindex];
					if (classname == null) {
						throw new IllegalStateException();
					}
					referencedClasses.add(classname);
					break;
				case CONSTANT_Methodref:
					int[] indexes = (int[]) cpdata[i];
					int classindex2 = indexes[0];
					int nameAndTypeIndex = indexes[1];
					StringBuilder s = new StringBuilder();
					String theClassName = (String) cpdata[(Integer) cpdata[classindex2]];
					if (theClassName.charAt(0) == 'j') {
						s.append(theClassName);
						s.append(".");
						s.append((String) cpdata[(Integer) cpdata[nameAndTypeIndex]]);
						referencedMethods.add(s.toString());
					}
					break;
				//			private final static byte CONSTANT_Utf8 = 1;
				//			private final static byte CONSTANT_Integer = 3;
				//			private final static byte CONSTANT_Float = 4;
				//			private final static byte CONSTANT_Long = 5;
				//			private final static byte CONSTANT_Double = 6;
				//			private final static byte CONSTANT_String = 8;
				//			private final static byte CONSTANT_Fieldref = 9;
				//			private final static byte CONSTANT_InterfaceMethodref = 11;
				//			private final static byte CONSTANT_NameAndType = 12;
			}
		}
	}

	public void readConstantPool(byte[] bytes) {
		try {
			ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
			DataInputStream dis = new DataInputStream(bais);

			int magic = dis.readInt(); // magic 0xCAFEBABE
			if (magic != 0xCAFEBABE) {
				throw new IllegalStateException("not bytecode, magic was 0x" + Integer.toString(magic, 16));
			}
			dis.skip(4); // skip minor and major versions
			cpsize = dis.readShort();
			if (DEBUG) {
				System.out.println("Constant Pool Size =" + cpsize);
			}
			cpdata = new Object[cpsize];
			type = new int[cpsize];
			for (int cpentry = 1; cpentry < cpsize; cpentry++) {
				boolean doubleSlot = processConstantPoolEntry(cpentry, dis);
				if (doubleSlot) {
					cpentry++;
				}
			}
			dis.skip(2); // access flags
			int thisclassname = dis.readShort();
			int classindex = ((Integer) cpdata[thisclassname]);
			slashedclassname = (String) cpdata[classindex];
		}
		catch (Exception e) {
			throw new IllegalStateException("Unexpected problem processing bytes for class", e);
		}
	}

	private boolean processConstantPoolEntry(int index, DataInputStream dis) throws IOException {
		byte b = dis.readByte();
		switch (b) {
			case CONSTANT_Utf8:
				// CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
				cpdata[index] = dis.readUTF();
				//			type[index] = b;
				if (DEBUG) {
					System.out.println(index + ":UTF8[" + cpdata[index] + "]");
				}
				break;
			case CONSTANT_Integer:
				// CONSTANT_Integer_info { u1 tag; u4 bytes; }
				if (DEBUG) {
					int i = dis.readInt();
					if (DEBUG) {
						System.out.println(index + ":INTEGER[" + i + "]");
					}
				}
				else {
					dis.skip(4);
				}
				break;
			case CONSTANT_Float:
				// CONSTANT_Float_info { u1 tag; u4 bytes; }
				if (DEBUG) {
					float f = dis.readFloat();
					if (DEBUG) {
						System.out.println(index + ":FLOAT[" + f + "]");
					}
				}
				else {
					dis.skip(4);
				}
				break;
			case CONSTANT_Long:
				//		CONSTANT_Long_info {
				//	    	u1 tag;
				//	    	u4 high_bytes;
				//	    	u4 low_bytes;
				//	    }
				if (DEBUG) {
					long l = dis.readLong();
					if (DEBUG) {
						System.out.println(index + ":LONG[" + l + "]");
					}
				}
				else {
					dis.skip(8);
				}
				return true;
			case CONSTANT_Double:
				//	    CONSTANT_Double_info {
				//	    	u1 tag;
				//	    	u4 high_bytes;
				//	    	u4 low_bytes;
				//	    }
				if (DEBUG) {
					double d = dis.readDouble();
					if (DEBUG) {
						System.out.println(index + ":DOUBLE[" + d + "]");
					}
				}
				else {
					dis.skip(8);
				}
				return true;
			case CONSTANT_Class:
				// CONSTANT_Class_info { u1 tag; u2 name_index; }
				type[index] = b;
				cpdata[index] = (int) dis.readShort();
				if (DEBUG) {
					System.out.println(index + ":CLASS[name_index=" + cpdata[index] + "]");
				}
				break;
			case CONSTANT_String:
				// CONSTANT_String_info { u1 tag; u2 string_index; }
				if (DEBUG) {
					cpdata[index] = (int) dis.readShort();
					if (DEBUG) {
						System.out.println(index + ":STRING[string_index=" + cpdata[index] + "]");
					}
				}
				else {
					dis.skip(2);
				}
				break;
			case CONSTANT_Fieldref:
				// CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
				if (DEBUG) {
					cpdata[index] = new int[] { dis.readShort(), dis.readShort() };
					if (DEBUG) {
						System.out.println(index + ":FIELDREF[class_index=" + ((int[]) cpdata[index])[0]
								+ ",name_and_type_index="
								+ ((int[]) cpdata[index])[1] + "]");
					}
				}
				else {
					dis.skip(4);
				}
				break;
			case CONSTANT_Methodref:
				// CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
				type[index] = b;
				//if (DEBUG) {
				cpdata[index] = new int[] { dis.readShort(), dis.readShort() };
				if (DEBUG) {
					System.out.println(index + ":METHODREF[class_index=" + ((int[]) cpdata[index])[0]
							+ ",name_and_type_index="
							+ ((int[]) cpdata[index])[1] + "]");
				}
				//			} else {
				//				dis.skip(4);
				//			}
				break;
			case CONSTANT_InterfaceMethodref:
				//			 CONSTANT_InterfaceMethodref_info {
				//			    	u1 tag;
				//			    	u2 class_index;
				//			    	u2 name_and_type_index;
				//			    }
				if (DEBUG) {
					cpdata[index] = new int[] { dis.readShort(), dis.readShort() };
					if (DEBUG) {
						System.out.println(index + ":INTERFACEMETHODREF[class_index=" + ((int[]) cpdata[index])[0]
								+ ",name_and_type_index=" + ((int[]) cpdata[index])[1] + "]");
					}
				}
				else {
					dis.skip(4);
				}
				break;
			case CONSTANT_NameAndType:
				// The CONSTANT_NameAndType_info structure is used to represent a field or method, without indicating which class or interface type it belongs to:
				// CONSTANT_NameAndType_info { u1 tag; u2 name_index; u2 descriptor_index; }
				//			type[index] = b;
				cpdata[index] = (int) dis.readShort();// new int[] { dis.readShort(), dis.readShort() };
				dis.skip(2); // skip the descriptor for now
				if (DEBUG) {
					System.out.println(index + ":NAMEANDTYPE[name_index=" + ((int[]) cpdata[index])[0]
							+ ",descriptor_index="
							+ ((int[]) cpdata[index])[1] + "]");
				}
				break;
			case CONSTANT_InvokeDynamic:
				//CONSTANT_InvokeDynamic_info {
				//	u1 tag;
				//	u2 bootstrap_method_attr_index;
				//	u2 name_and_type_index;
				//}
				dis.skipBytes(4);
				break;
			case CONSTANT_MethodHandle:
				//CONSTANT_MethodHandle_info {
				//	u1 tag;
				//	u1 reference_kind;
				//	u2 reference_index;
				//}
				dis.skipBytes(3);
				break;
			case CONSTANT_MethodType:
				//CONSTANT_MethodType_info {
				//	u1 tag;
				//	u2 descriptor_index;
				//}
				dis.skipBytes(2);
				break;
			default:
				throw new IllegalStateException("Entry: " + index + " " + Byte.toString(b));
		}
		return false;
	}
}
